{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "data_file = 'ratings.csv'\n",
    "\n",
    "\n",
    "class SlopeOneCF:\n",
    "    def __init__(self):\n",
    "        self.data = pd.read_csv(data_file, usecols=range(3))\n",
    "        self.data.columns = ['user', 'item', 'rating']\n",
    "        self.train = {}\n",
    "        self.test = {}\n",
    "        self.frequencies = {}\n",
    "        self.deviations = {}\n",
    "\n",
    "    @staticmethod\n",
    "    def _process_data(input_data):\n",
    "        \"\"\"\n",
    "        自定义数据处理函数\n",
    "        :param input_data: DataFrame\n",
    "        :return: dict{user_id: {item_id: rating}}\n",
    "        \"\"\"\n",
    "        output_data = {}\n",
    "        for _, items in input_data.iterrows():\n",
    "            user = int(items['user'])\n",
    "            item = int(items['item'])\n",
    "            rating = float(items['rating'])\n",
    "            if user in output_data.keys():\n",
    "                currentRatings = output_data[user]\n",
    "            else:\n",
    "                currentRatings = {}\n",
    "            currentRatings[item] = rating\n",
    "            output_data[user] = currentRatings\n",
    "        return output_data\n",
    "\n",
    "    def load_data(self, train_size, normalize):\n",
    "        \"\"\"\n",
    "        划分训练集、测试集，并定义数据结构为：dict{user_id: {item_id: rating}}\n",
    "        :param train_size:\n",
    "        :param normalize:\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        print('loading data')\n",
    "        if normalize:\n",
    "            # 利用pandas对整列进行归一化，评分在(0,1)之间\n",
    "            rating = self.data['rating']\n",
    "            self.data['rating'] = (rating - rating.min()) / (rating.max() - rating.min())\n",
    "\n",
    "        train_data = self.data.sample(frac=train_size, random_state=10, axis=0)\n",
    "        test_data = self.data[~self.data.index.isin(train_data.index)]\n",
    "\n",
    "        self.train = self._process_data(train_data)\n",
    "        self.test = self._process_data(test_data)\n",
    "\n",
    "        print('loaded data finish')\n",
    "\n",
    "    def compute_deviations(self):\n",
    "        \"\"\"\n",
    "        计算物品和物品之间的相似度矩阵，矩阵样式可参考pandas的pivot_table透视表\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        print('computing all deviations')\n",
    "        for ratings in self.train.values():\n",
    "            for (item, rating) in ratings.items():\n",
    "                self.frequencies.setdefault(item, {})\n",
    "                self.deviations.setdefault(item, {})\n",
    "                for (item2, rating2) in ratings.items():\n",
    "                    if item != item2:\n",
    "                        self.frequencies[item].setdefault(item2, 0)\n",
    "                        self.deviations[item].setdefault(item2, 0.0)\n",
    "                        self.frequencies[item][item2] += 1\n",
    "                        self.deviations[item][item2] += rating - rating2\n",
    "\n",
    "        for (item, ratings) in self.deviations.items():\n",
    "            for item2 in ratings:\n",
    "                ratings[item2] /= self.frequencies[item][item2]\n",
    "        print('computed all deviations finish')\n",
    "\n",
    "    def predict(self, userRatings):\n",
    "        \"\"\"\n",
    "        对用户进行推荐结果\n",
    "        :param userRatings:\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        recommendations = {}  # 存储推荐结果\n",
    "        frequencies = {}  # 物品被不同用户访问的记录\n",
    "        for (userItem, userRating) in userRatings.items():\n",
    "            for (diffItem, diffRatings) in self.deviations.items():\n",
    "                if diffItem not in userRatings and \\\n",
    "                        userItem in self.deviations[diffItem]:\n",
    "                    freq = self.frequencies[diffItem][userItem]\n",
    "                    recommendations.setdefault(diffItem, 0.0)\n",
    "                    frequencies.setdefault(diffItem, 0)\n",
    "                    recommendations[diffItem] += (diffRatings[userItem] + userRating) * freq\n",
    "                    frequencies[diffItem] += freq\n",
    "        for (k, v) in recommendations.items():\n",
    "            recommendations[k] = v / frequencies[k]\n",
    "        return recommendations\n",
    "\n",
    "    def validation(self):\n",
    "        \"\"\"\n",
    "        计算MAE、RMSE评估指标\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        print('calculating MAE and RMSE')\n",
    "        error_sum = 0.0\n",
    "        sqrError_sum = 0.0\n",
    "        setSum = 0\n",
    "        count = 0\n",
    "        i = 0\n",
    "        for user in self.test:\n",
    "            i += 1\n",
    "            if i % 100 == 0:\n",
    "                print('calculating %d users' % i)\n",
    "            recommendation = self.predict(self.train[user]).copy()\n",
    "            count += len(recommendation.items())\n",
    "            userRatings = self.test[user]\n",
    "            for item in recommendation:\n",
    "                if item in userRatings:\n",
    "                    error_sum += abs(userRatings[item] - recommendation[item])\n",
    "                    sqrError_sum += (userRatings[item] - recommendation[item]) ** 2\n",
    "                    setSum += 1\n",
    "        mae = error_sum / setSum\n",
    "        rmse = np.sqrt(sqrError_sum / setSum)\n",
    "        return mae, rmse\n",
    "\n",
    "    def evaluate(self):\n",
    "        \"\"\"\n",
    "        根据测试集中所有用户进行推荐topN结果，并计算precision和recall\n",
    "        :param N:\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        print('calculating top N result')\n",
    "        hit = 0\n",
    "        recall_sum = 0\n",
    "        precision_sum = 0\n",
    "        i = 0\n",
    "        for user in self.test:\n",
    "            i += 1\n",
    "            if i % 100 == 0:\n",
    "                print('calculating %d users' % i)\n",
    "            real_items = self.test.get(user)  # 真实的items\n",
    "            recommendation = self.predict(self.train[user]).copy()\n",
    "            item_list = [(item, rating) for item, rating in recommendation.items()]\n",
    "            item_list.sort(key=lambda x: x[1], reverse=True)\n",
    "            pred_items = [i[0] for i in item_list]\n",
    "\n",
    "            hit = len([i for i in pred_items if i in real_items])  # 预测正确的items\n",
    "\n",
    "            precision_sum += len(pred_items)\n",
    "            recall_sum += len(real_items)\n",
    "\n",
    "        precision = hit / (1.0 * precision_sum)\n",
    "        recall = hit / (1.0 * recall_sum)\n",
    "\n",
    "        return precision, recall\n",
    "\n",
    "    def get_top_n(self, user, top_n=10):\n",
    "        recommendation = self.slope_one_recommend(self.train[user]).copy()\n",
    "        item_list = [(item, rating) for item, rating in recommendation.items()]\n",
    "        item_list.sort(key=lambda x: x[1], reverse=True)\n",
    "        top_list = item_list[:top_n]  # 预测的items\n",
    "        return top_list\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loading data\n",
      "loaded data finish\n",
      "computing all deviations\n",
      "computed all deviations finish\n"
     ]
    }
   ],
   "source": [
    "slope_one = SlopeOneCF()\n",
    "slope_one.load_data(train_size=0.8, normalize=False)\n",
    "slope_one.compute_deviations()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "calculating MAE and RMSE\n",
      "calculating 100 users\n",
      "calculating 200 users\n",
      "calculating 300 users\n",
      "calculating 400 users\n",
      "calculating 500 users\n",
      "calculating 600 users\n",
      "MAE: 0.6656053452250917 RMSE: 0.8734252779221545\n"
     ]
    }
   ],
   "source": [
    "mae, rmse = slope_one.validation()\n",
    "print('MAE:', mae, 'RMSE:', rmse)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "calculating top N result\n"
     ]
    },
    {
     "ename": "AttributeError",
     "evalue": "'SlopeOneCF' object has no attribute 'predict'",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-10-5ac4dd6435e5>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mpre\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mrec\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mslope_one\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mevaluate\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m      2\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'precision:'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mpre\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'recall:'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mrec\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
      "\u001b[1;32m<ipython-input-7-5af40c0acf40>\u001b[0m in \u001b[0;36mevaluate\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    140\u001b[0m                 \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'calculating %d users'\u001b[0m \u001b[1;33m%\u001b[0m \u001b[0mi\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    141\u001b[0m             \u001b[0mreal_items\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtest\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0muser\u001b[0m\u001b[1;33m)\u001b[0m  \u001b[1;31m# 真实的items\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 142\u001b[1;33m             \u001b[0mrecommendation\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mpredict\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtrain\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0muser\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcopy\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m    143\u001b[0m             \u001b[0mitem_list\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m[\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mitem\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mrating\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mitem\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mrating\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrecommendation\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mitems\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    144\u001b[0m             \u001b[0mitem_list\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msort\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mlambda\u001b[0m \u001b[0mx\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mx\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mreverse\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mTrue\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
      "\u001b[1;31mAttributeError\u001b[0m: 'SlopeOneCF' object has no attribute 'predict'"
     ]
    }
   ],
   "source": [
    "pre, rec = slope_one.evaluate()\n",
    "print('precision:', pre, 'recall:', rec)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[(136850, 8.083333333333334), (124851, 8.0), (114265, 8.0), (103543, 8.0), (113829, 8.0), (103602, 7.666666666666667), (109241, 7.666666666666667), (149508, 7.666666666666667), (118270, 7.666666666666667), (142444, 7.666666666666667)]\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>item</th>\n",
       "      <th>rating</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>136850</td>\n",
       "      <td>8.083333</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>124851</td>\n",
       "      <td>8.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>114265</td>\n",
       "      <td>8.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>103543</td>\n",
       "      <td>8.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>113829</td>\n",
       "      <td>8.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>103602</td>\n",
       "      <td>7.666667</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>109241</td>\n",
       "      <td>7.666667</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>149508</td>\n",
       "      <td>7.666667</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>118270</td>\n",
       "      <td>7.666667</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>142444</td>\n",
       "      <td>7.666667</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "     item    rating\n",
       "0  136850  8.083333\n",
       "1  124851  8.000000\n",
       "2  114265  8.000000\n",
       "3  103543  8.000000\n",
       "4  113829  8.000000\n",
       "5  103602  7.666667\n",
       "6  109241  7.666667\n",
       "7  149508  7.666667\n",
       "8  118270  7.666667\n",
       "9  142444  7.666667"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "result = slope_one.get_top_n(user=1, top_n=10)\n",
    "print(result)\n",
    "pd.DataFrame(result, columns=['item', 'rating'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
